/*
 * Copyright (c) 2021-2021. Gardel
 *
 * This file is part of Source2Pdf (https://gitee.com/gardel/source2pdf).
 *
 * Source2Pdf is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Source2Pdf is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Source2Pdf.  If not, see <http://www.gnu.org/licenses/>.
 */

package cn.sunxinao.source2pdf.controller;

import cn.sunxinao.source2pdf.MainApp;
import cn.sunxinao.source2pdf.model.FileNode;
import cn.sunxinao.source2pdf.utils.TaskUtils;
import cn.sunxinao.source2pdf.view.FileExplorerTreeCell;
import cn.sunxinao.source2pdf.view.FileListCell;
import io.github.palexdev.materialfx.controls.MFXButton;
import io.github.palexdev.materialfx.controls.MFXCheckTreeItem;
import io.github.palexdev.materialfx.controls.MFXCheckTreeView;
import io.github.palexdev.materialfx.controls.MFXComboBox;
import io.github.palexdev.materialfx.controls.MFXDialog;
import io.github.palexdev.materialfx.controls.MFXFlowlessListView;
import io.github.palexdev.materialfx.controls.MFXIconWrapper;
import io.github.palexdev.materialfx.controls.MFXRadioButton;
import io.github.palexdev.materialfx.controls.MFXStageDialog;
import io.github.palexdev.materialfx.controls.MFXTextField;
import io.github.palexdev.materialfx.controls.MFXToggleButton;
import io.github.palexdev.materialfx.controls.MFXTooltip;
import io.github.palexdev.materialfx.controls.MFXTreeItem;
import io.github.palexdev.materialfx.controls.base.AbstractMFXTreeCell;
import io.github.palexdev.materialfx.controls.base.AbstractMFXTreeItem;
import io.github.palexdev.materialfx.controls.enums.DialogType;
import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
import io.github.palexdev.materialfx.controls.factories.MFXDialogFactory;
import io.github.palexdev.materialfx.effects.DepthLevel;
import io.github.palexdev.materialfx.font.MFXFontIcon;
import io.github.palexdev.materialfx.utils.DialogUtils;
import io.github.palexdev.materialfx.utils.NodeUtils;
import io.github.palexdev.materialfx.utils.ScrollUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.print.Printer;
import javafx.print.PrinterJob;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import javafx.util.Callback;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;

public class MainController implements Initializable {
    private final Stage primaryStage;

    private final Callback<AbstractMFXTreeItem<FileNode>, AbstractMFXTreeCell<FileNode>> cellFactory;

    @FXML
    private HBox rootPane;

    @FXML
    private AnchorPane fileExplorePane;

    @FXML
    private Label fileChoosePath;

    @FXML
    private AnchorPane fileListPane;

    @FXML
    private AnchorPane controlPane;

    @FXML
    private MFXCheckTreeView<FileNode> explorerTreeView;

    @FXML
    private MFXFlowlessListView<FileNode> listView;

    @FXML
    private MFXComboBox<String> fontComboBox;

    @FXML
    private WebView previewWebView;

    @FXML
    private MFXTextField fontSizeField;

    @FXML
    private MFXToggleButton trimEmptyLinesCheck;

    @FXML
    private MFXButton previewBtn;

    @FXML
    private MFXButton printBtn;

    @FXML
    private HBox windowButtons;

    private Printer usePrinter;

    public MainController(Stage primaryStage) {
        this.primaryStage = primaryStage;
        this.cellFactory = item -> new FileExplorerTreeCell((MFXCheckTreeItem<FileNode>) item);
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        initializeWindowIcons();

        ChangeListener<Number> rootResizeListener = (observable, oldValue, newValue) -> {
            double width = primaryStage.getWidth();
            double height = primaryStage.getHeight();
            if (Double.isNaN(width) || Double.isNaN(height)) return;
            rootPane.setPrefSize(width, height);
            double subWidth = width / 3;
            if (subWidth <= fileExplorePane.getMinWidth()) {
                fileExplorePane.setPrefSize(fileExplorePane.getMinWidth(), height);
                double v = (width - fileExplorePane.getMinWidth()) / 2.0;
                fileListPane.setPrefSize(v, height);
                controlPane.setPrefSize(v, height);
            } else {
                fileExplorePane.setPrefSize(subWidth, height);
                fileListPane.setPrefSize(subWidth, height);
                controlPane.setPrefSize(subWidth, height);
            }
        };
        primaryStage.widthProperty().addListener(rootResizeListener);
        primaryStage.heightProperty().addListener(rootResizeListener);

        fileChoosePath.textProperty().addListener(this::onDirectoryChosen);
        fileChoosePath.textProperty().addListener((observable, old, newValue) -> {
            if (newValue == null || newValue.isBlank()) {
                fileChoosePath.setTooltip(null);
            } else {
                fileChoosePath.setTooltip(new MFXTooltip(newValue));
            }
        });

        listView.setCellFactory(fileNode -> new FileListCell(listView, fileNode));
        listView.getItems().addListener((ListChangeListener<FileNode>) c -> {
            if (c.getList().isEmpty()) {
                previewBtn.setDisable(true);
                printBtn.setDisable(true);
            } else {
                previewBtn.setDisable(false);
                printBtn.setDisable(false);
            }
        });

        explorerTreeView.getCheckModel().getCheckedItems().addListener((ListChangeListener<MFXCheckTreeItem<FileNode>>) c -> {
            while (c.next()) {
                if (c.wasAdded()) {
                    for (var addItem : c.getAddedSubList()) {
                        var data = addItem.getData();
                        if (data.isFile()) {
                            listView.getItems().add(data);
                        }
                    }
                } else if (c.wasRemoved()) {
                    for (var remItem : c.getRemoved()) {
                        var data = remItem.getData();
                        if (data.isFile()) {
                            listView.getItems().remove(data);
                        }
                    }
                }
            }
        });

        TaskUtils.submitTask(() -> {
            List<String> fontNames = Font.getFontNames();
            Platform.runLater(() -> {
                fontComboBox.getItems().addAll(fontNames);
                fontComboBox.setSelectedValue(Font.getDefault().getName());
            });
        });

        fontSizeField.setTextFormatter(new TextFormatter<String>(change -> {
            if (change.isAdded() || change.isReplaced()) {
                if (StringUtils.isNumeric(change.getText())) {
                    return change;
                } else {
                    return null;
                }
            }
            return change;
        }));
        fontSizeField.textProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue.isBlank()) {
                fontSizeField.setText("12");
            }
        });
    }

    public void onChooseFileClicked(ActionEvent actionEvent) {
        actionEvent.consume();
        DirectoryChooser directoryChooser = new DirectoryChooser();
        File oldPath = new File(fileChoosePath.getText());
        if (oldPath.exists()) directoryChooser.setInitialDirectory(oldPath);
        File file = directoryChooser.showDialog(primaryStage);
        if (file != null) {
            fileChoosePath.setText(file.getAbsolutePath());
        }
    }

    public void onClickListClicked(ActionEvent actionEvent) {
        actionEvent.consume();
        explorerTreeView.getCheckModel().clearChecked();
    }

    public void onSortListClicked(ActionEvent actionEvent) {
        actionEvent.consume();
        listView.getItems().sort((up, down) -> {
            byte[] upName = up.getDisplayName().getBytes(Charset.forName("GBK"));
            byte[] downName = down.getDisplayName().getBytes(Charset.forName("GBK"));
            return Arrays.compare(upName, downName);
        });
    }

    public void onPreviewClicked(ActionEvent actionEvent) {
        actionEvent.consume();
        WebEngine webViewEngine = previewWebView.getEngine();
        try (InputStream inputStream = MainApp.class.getResourceAsStream("/img/loading.svg")) {
            if (inputStream == null) {
                webViewEngine.loadContent("<h1>加载中</h1>");
            } else {
                webViewEngine.loadContent(new String(inputStream.readAllBytes()));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        String fontName = fontComboBox.getSelectedValue();
        Font font = new Font(fontName, Double.parseDouble(fontSizeField.getText()));
        String familyCss = String.format("\"%s\", \"%s\"",
            font.getName().replace("\"", "\\"),
            font.getFamily().replace("\"", "\\"));
        boolean trimEmptyLines = trimEmptyLinesCheck.isSelected();
        TaskUtils.submitTask(() -> {
            StringBuilder htmlContent = new StringBuilder();
            htmlContent.append("""
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <title>源代码</title>
                    <style>
                        pre {
                            font-family:
                """)
                .append(familyCss)
                .append("""
                ;
                            font-size:""")
                .append(font.getSize())
                .append("""
                px;
                        }
                        
                        @page {
                            size: A4;
                        }
                    </style>
                </head>
                <body>
                <pre>
                """);

            List<FileNode> items = listView.getItems();
            List<Exception> errors = new LinkedList<>();
            for (FileNode item : items) {
                if (item.isDirectory()) {
                    continue;
                }
                try (var stream = Files.lines(item.getPath())) {
                    stream.filter(s -> !trimEmptyLines || !s.isBlank())
                        .map(StringEscapeUtils::escapeHtml4)
                        .forEach(str -> htmlContent.append(str).append("\n"));
                } catch (IOException e) {
                    errors.add(e);
                } catch (UncheckedIOException e) {
                    errors.add(e.getCause());
                }
            }

            htmlContent.append("""
                </pre>
                </body>
                </html>
                """);

            Platform.runLater(() -> webViewEngine.loadContent(htmlContent.toString()));
            if (!errors.isEmpty()) {
                String content = errors.stream().map(Throwable::getLocalizedMessage)
                    .collect(Collectors.joining("\n"));
                MFXStageDialog dialog = DialogUtils.getStageDialog(primaryStage, DialogType.ERROR, "异常", content);
                Platform.runLater(dialog::show);
            }
        });
    }

    public void onPrintClicked(ActionEvent actionEvent) {
        actionEvent.consume();
        ToggleGroup toggleGroup = new ToggleGroup();
        MFXFlowlessListView<MFXRadioButton> btnListView =
            new MFXFlowlessListView<>();
        btnListView.setDepthLevel(DepthLevel.LEVEL1);
        ObservableList<MFXRadioButton> items = btnListView.getItems();
        Printer defaultPrinter = Printer.getDefaultPrinter();
        ObservableSet<Printer> allPrinters = Printer.getAllPrinters();
        for (Printer printer : allPrinters) {
            MFXRadioButton radioButton = new MFXRadioButton(printer.getName());
            items.add(radioButton);
            radioButton.setToggleGroup(toggleGroup);
            radioButton.setUserData(printer);
            if (usePrinter != null && usePrinter.equals(printer)
                || usePrinter == null && printer.equals(defaultPrinter)) {
                radioButton.setSelected(true);
            }
        }
        if (toggleGroup.getSelectedToggle() == null && !toggleGroup.getToggles().isEmpty()) {
            toggleGroup.selectToggle(toggleGroup.getToggles().get(0));
        }
        MFXDialog dialog = new MFXDialog();
        dialog.setPrefHeight(400.0);
        URL url = MainApp.class.getResource("css/main.css");
        if (url != null) {
            dialog.getStylesheets().add(url.toString());
        }
        dialog.setInAnimationType(MFXAnimationFactory.FADE_IN);
        dialog.setOutAnimationType(MFXAnimationFactory.FADE_OUT);
        dialog.setAnimateIn(true);
        dialog.setAnimateOut(true);
        MFXDialogFactory.setHeaderNode(DialogType.INFO, dialog);
        ((Region) dialog.getTop()).setPrefHeight(dialog.getPrefHeight() * 0.3333);
        StackPane contentNode = new StackPane();
        contentNode.prefWidthProperty().bind(dialog.widthProperty());
        btnListView.prefWidthProperty().bind(contentNode.widthProperty());
        btnListView.setPrefHeight(300.0);
        Label titleLabel = new Label();
        titleLabel.getStyleClass().setAll("title-label");
        titleLabel.textProperty().bind(dialog.titleProperty());
        dialog.setTitle("选择打印机");
        StackPane.setAlignment(titleLabel, Pos.TOP_CENTER);
        StackPane.setMargin(titleLabel, new Insets(15, 0, 0, 0));
        StackPane.setAlignment(btnListView, Pos.TOP_CENTER);
        StackPane.setMargin(btnListView, new Insets(40, 8, 15, 8));
        MFXButton actionButton = new MFXButton("确定");
        actionButton.getStyleClass().add("btn");
        HBox hBox = new HBox(10, actionButton);
        hBox.setAlignment(Pos.CENTER);
        hBox.setPrefHeight(60);
        hBox.prefWidthProperty().bind(dialog.widthProperty());
        dialog.setActions(hBox);
        contentNode.getChildren().addAll(titleLabel, btnListView);
        dialog.setCenter(contentNode);
        dialog.setType(DialogType.INFO);
        MFXStageDialog stageDialog = DialogUtils.getStageDialog(primaryStage, dialog);
        actionButton.setOnAction(event -> {
            usePrinter = (Printer) toggleGroup.getSelectedToggle().getUserData();
            PrinterJob printerJob = PrinterJob.createPrinterJob(usePrinter);
            if (printerJob.showPageSetupDialog(primaryStage)) {
                previewWebView.getEngine().print(printerJob);
                printerJob.endJob();
                dialog.close();
                stageDialog.close();
            }
        });
        stageDialog.show();
    }

    private void onDirectoryChosen(Observable path, String old, String newValue) {
        Path rootPath = Path.of(newValue);
        if (Files.notExists(rootPath)) return;
        MFXCheckTreeItem<FileNode> root = new MFXCheckTreeItem<>(new FileNode(rootPath), cellFactory);
        root.setTooltip(new MFXTooltip(root.getData().getDisplayName()));
        buildTree(rootPath, root);
        explorerTreeView.getCheckModel().clearChecked();
        explorerTreeView.setRoot(root);
        explorerTreeView.setupRoot();
        ScrollUtils.addSmoothScrolling(explorerTreeView);
    }

    private void buildTree(Path basePath, MFXCheckTreeItem<FileNode> root) {
        FileNode data = root.getData();
        if (data.isDirectory()) {
            var items = root.getItems();
            TaskUtils.submitTask(() -> {
                List<MFXCheckTreeItem<FileNode>> treeItems = data.getChildren()
                    .map(node -> new MFXCheckTreeItem<>(node, cellFactory))
                    .collect(Collectors.toList());
                Platform.runLater(() -> items.addAll(treeItems));
            });
            root.expandedProperty().addListener((observable, oldValue, newValue) -> {
                if (newValue) {
                    for (var child : items) {
                        String filename = child.getData().getFileName();
                        child.setTooltip(new MFXTooltip(child.getData().getDisplayName()));
                        Path subPath = basePath.resolve(filename);
                        buildTree(subPath, (MFXCheckTreeItem<FileNode>) child);
                    }
                } else {
                    for (var child : items) {
                        child.getItems().clear();
                    }
                }
            });
            root.checkedProperty().addListener((observable, oldValue, newValue) -> {
                if (newValue) {
                    expandTree(root);
                }
            });
        }
    }

    private void expandTree(MFXTreeItem<FileNode> root) {
        ObservableList<Node> children = root.getChildrenUnmodifiable();
        if (children.isEmpty()) return;
        var vbox = (VBox) children.get(0);
        children = vbox.getChildren();
        if (children.isEmpty()) return;
        var node = children.get(0);
        if (!(node instanceof FileExplorerTreeCell cell)) return;
        MFXIconWrapper disclosureNode = cell.getDisclosureNode();
        Platform.runLater(() -> {
            if (!root.isExpanded()) {
                NodeUtils.fireDummyEvent(disclosureNode);
            }
        });
        // TODO 展开所有子节点
    }

    private void initializeWindowIcons() {
        MFXFontIcon xIcon = new MFXFontIcon("mfx-x-circle", 16);
        MFXFontIcon minusIcon = new MFXFontIcon("mfx-minus-circle", 16);
        MFXFontIcon expandIcon = new MFXFontIcon("mfx-expand", 12.5);
        MFXIconWrapper closeButton = new MFXIconWrapper(xIcon, 22);
        closeButton.setId("closeButton");
        closeButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> primaryStage.close());
        MFXIconWrapper minimizeButton = new MFXIconWrapper(minusIcon, 22);
        minimizeButton.setId("minimizeButton");
        minimizeButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> primaryStage.setIconified(true));
        MFXIconWrapper expandButton = new MFXIconWrapper(expandIcon, 22);
        expandButton.setId("expandButton");
        expandButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> primaryStage.setFullScreen(!primaryStage.isFullScreen()));
        NodeUtils.makeRegionCircular(closeButton);
        NodeUtils.makeRegionCircular(minimizeButton);
        NodeUtils.makeRegionCircular(expandButton);
        windowButtons.getChildren().addAll(expandButton, minimizeButton, closeButton);
    }
}
